using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;
using System.Threading;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Stunt.Nutss;

namespace Stunt.Nutss.Signaling
{

	/// <summary>
	/// Implements a directory server and signaling proxy. Clients register with the directory by sending a HELLO message. Clients can
	/// request a packet to be forwarded to another client by sending a ROUTE message.
	/// between clients.
	/// </summary>
	public class SlingServer
	{

		#region Variables

		/// <summary> Timeout for ID registrations in seconds</summary>
		internal const int TIMEOUT = 300;

		/// <summary>Stores client to address maps, and other details.</summary>
		private Dictionary<NutssUri, DirectoryEntry> directory = null;

		/// <summary>UDP socket for proxy communication.</summary>
		private Socket server;

		/// <summary>STUNT Responders</summary>
		private EndPoint[] responders;

		#endregion

		#region Constructor

		/// <summary>Creates a new proxy server</summary>
		/// <param name="port">UDP port on which to listen</param>
		/// <param name="addrs">IP address and TCP ports of STUNT Responders</param>
		public SlingServer(int port, EndPoint[] addrs)
		{
			directory = new Dictionary<NutssUri, DirectoryEntry>();
			server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
			server.Bind(new IPEndPoint(IPAddress.Any, port));
			responders = addrs;
		}

		#endregion

		#region Properties

		/// <summary>Returns the local IP and port the server is listening on.</summary>
		/// <returns>Server socket address</returns>
		public EndPoint EndPoint
		{
			get
			{
				return server.LocalEndPoint;
			}
		}

		#endregion

		#region ThreadProcess

		/// <summary>Starts the proxy server.</summary>
		public void ThreadProcess()
		{
			EndPoint addr;
			DirectoryEntry src = null;
			DirectoryEntry dst = null;
			DateTime expires;
			SlingMessage msg;
			BinaryFormatter formatter = new BinaryFormatter();
			byte[] buf = new byte[1472];

			try
			{

				while (true)
				{
					// Creates an IpEndPoint to capture the identity of the sending host.
					EndPoint senderRemote = new IPEndPoint(IPAddress.Any, 0);
					int received = server.ReceiveFrom(buf, ref senderRemote);
                    IPEndPoint receiveEndPoint = (IPEndPoint)senderRemote;

					// Should be 1472
					if (received == 0) continue;
					addr = senderRemote;
					Console.WriteLine(addr);

					// Read the request
					try
					{
						msg = (SlingMessage)formatter.Deserialize(new MemoryStream(buf, 0, received));
					}

					catch (Exception e)
					{
						Console.WriteLine("JSlllingServer - ThreadProcess" + e.Message);
						continue;
					}
					Console.WriteLine(String.Format("RECEIVED: addr={0} size={1} msg={2}", addr, received, msg));

					switch (msg.type)
					{

						// Client registration request. Create a client entry or update expiry timers.
						case SlingMessage.Type.JSLING_REGISTER:
							Console.WriteLine("Proccessing Registration");

							if (directory.ContainsKey(msg.Source))
								src = directory[msg.Source];
							// src = directory[msg.Source];                            
							expires = new DateTime(TIMEOUT * 1000 + (DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond));

							if (src == null || src.expires < DateTime.Now)
							{
                                Console.WriteLine("Allow client to register new or expired ID");
								// Allow client to register new or expired ID
								directory.Add(msg.Source, new DirectoryEntry(msg.Source, receiveEndPoint, expires));
								Console.WriteLine(msg.Source + " is " + receiveEndPoint);
								msg.response = SlingMessage.Response.JSLING_SUCCESS;
								msg.payload = responders;
							}

							else if (src.address.Address == receiveEndPoint.Address)
							{
								// Allow client to refresh own registration
								// as long as IP address matches
                                Console.WriteLine("Allow client to refresh own registration");
								src.expires = expires;

								if (src.address != receiveEndPoint)
									Console.WriteLine(msg.Source + " now is " + receiveEndPoint);
								src.address = receiveEndPoint;
								msg.response = SlingMessage.Response.JSLING_SUCCESS;
								msg.payload = responders;
							}

							else
							{
								// Prevent clients from pre-empting IDs from different IPs
								msg.response = SlingMessage.Response.JSLING_ERROR;
							}
							break;

						// Forward to destination in one hop. Return error otherwise.
						case SlingMessage.Type.JSLING_FORWARD:
							Console.WriteLine("Proccessing FORWARD");

							//dst = directory[msg.response == null ? msg.destination : msg.Source];
							if (directory.ContainsKey(msg.Source))
								dst = directory[msg.Source];

							if (dst == null || dst.expires < DateTime.Now)
							{
								// No loops, if not locally found - report error
								msg.response = SlingMessage.Response.JSLING_ERROR;
							}

							else
							{
								// one-hop forward to destination
								addr = dst.address;
							}
							break;

						// Route to destination (maybe one hop). Error if sender is not registered.
						case SlingMessage.Type.JSLING_ROUTE:
							Console.WriteLine("Proccessing ROUTE");

							//src = directory[msg.response == null ? msg.Source : msg.destination];
							//dst = directory[msg.response == null ? msg.Destination : msg.Source];
							if (directory.ContainsKey(msg.destination))
							{
								src = directory[msg.destination];
							}

							if (directory.ContainsKey(msg.Source))
							{
								src = directory[msg.Source];
							}

							if (src == null || src.address != receiveEndPoint || src.expires < DateTime.Now)
							{
								// only allow registered sources to send multi-hop packets
								msg.response = SlingMessage.Response.JSLING_ERROR;
							}

							else if (dst == null || dst.expires < DateTime.Now)
							{

								// find authoritative proxy for destination
								try
								{
									addr = (IPEndPoint)SlingCommon.ResolveProxy(msg.Destination);
								}

								catch (ExtendedSocketException e)
								{
									addr = null;
								}

								if (addr == null)
								{
									addr = receiveEndPoint;
									msg.response = SlingMessage.Response.JSLING_ERROR;
								}

								else
								{
									// trapezoid routing. prevent loops
									msg.type = SlingMessage.Type.JSLING_FORWARD;
								}
							}

							else
							{
								// found in local table
								addr = dst.address;
							}
							break;
						default:
							msg.response = SlingMessage.Response.JSLING_ERROR;
							break;
					}
					// Send packet
					MemoryStream stream = new MemoryStream();

					formatter.Serialize(stream, msg);
					msg.bbuf = stream;

					if (msg.bbuf != null) Console.WriteLine("bbuf: " + msg.bbuf.Length + " addr: " + addr);
					server.SendTo(msg.bbuf.GetBuffer(), (int)msg.bbuf.Length, SocketFlags.None, addr);
					// Send packet.
					Console.WriteLine(String.Format("SENT: addr={0} size={1} msg={2}", addr, stream.Length, msg));
				}
			}

			catch (Exception e)
			{
				Console.WriteLine("JSlingServer - STACK: " + e.StackTrace);
				Console.WriteLine("JSlingServer - MESSAGE: " + e.Message);
			}

			finally
			{

				try
				{
					server.Close();
				}

				catch (Exception e) { }
			}
		}

		#endregion

		#region Main

		/*

        /// <summary>Starts the server</summary>
        /// <param name="args">Program arguments</param>
        [STAThread]
        public static void Main(string[] args)
        {
            STUNTServer r1, r2;
            SlingServer p;

            if (args.Length != 2)
            {
                System_Renamed.err.printf("Usage:\n\t JSlingServer ip1 ip2\n" + "The two IP Addresses should be different and belong to local interfaces.\n");
                System.Environment.Exit(1);
            }

            try
            {
                r1 = new STUNTServer(Dns.Resolve(args[0]).AddressList[0], PROXY_PORT + 16 + (int)(10 * new Random().NextDouble()));
                r2 = new STUNTServer(Dns.Resolve(args[1]).AddressList[0], PROXY_PORT + 32 + (int)(10 * new Random().NextDouble()));
                EndPoint[] addrs = new EndPoint[] { r1.EndPoint, r2.EndPoint };
                p = new SlingServer(PROXY_PORT, addrs);
                Console.WriteLine("STUNT Server 1: %s\n", r1.EndPoint);
                Console.WriteLine("STUNT Server 2: %s\n", r2.EndPoint);
                Console.WriteLine("Directory  : %s\n", p.EndPoint);
                new Thread(new ThreadStart(r1.ThreadProcess)).Start();
                new Thread(new ThreadStart(r2.ThreadProcess)).Start();
                new Thread(new ThreadStart(p.ThreadProcess)).Start();
            }

            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }
        }
		*/

		#endregion

	}
}